<?php

namespace liketp;

use liketp\exception\ErrorException;
use liketp\exception\RuntimeException;
use liketp\exception\Handler as ExceptionHandler;
use liketp\foundation\Timer;
use liketp\socket\EventsHandle;
use Throwable;

/**
 * 框架核心入口
 * 负责管理框架的核心功能，主要通过容器提供各种资源
 */
class App extends Container
{
    protected static $instance;
    protected $initialized = false;

    protected $thinkPath = '';
    protected $rootPath = '';
    protected $appPath = '';
    protected $entrancePath = '';
    protected $configPath = '';
    protected $runtimePath = '';
    protected $routePath = '';
    protected $viewPath = '';

    protected $appForder = 'app';
    protected $appNamespace = 'app';

    protected $defaultController = DefaultController::class;

    protected $env = [];

    protected $bindings = [
        'app' => App::class,
        'config' => Config::class,
        'http' => Http::class,
        'console' => Console::class,
        'request' => Request::class,
        'response' => Response::class,
        'middleware' => Middleware::class,
        'db' => DatabaseManager::class,
        'db.connector' => database\Connector::class,
        'route' => Route::class,
        'event' => Event::class,
        'timer' => Timer::class,
        'socket.event_handle' => EventsHandle::class,
    ];


    //------------------------------------------------ initialize
    protected function __construct()
    {
        parent::__construct();

        $this->setInstance('app', $this);
        $this->setInstance('container', $this);
        $this->setInstance(Container::class, $this);

        $this->prepare();
        $this->ditectPaths();
        $this->registerExceptionHandler();

        include_once('helper.php');
    }

    public function initialize()
    {
        if ($this->initialized()) return;
        $this->initialized = true;

        $this->loadFiles();

        $this->initCoreService();
        $this->bootCoreService();

        if ($this->isCli()) {
            ob_end_flush();
        }
    }

    public function initialized()
    {
        return $this->initialized;
    }

    public function debug()
    {
        $this->env['debug'] = true;
    }

    public function isDebug()
    {
        $configDebug = $this->config('app.debug') ?? null;
        $envDebug = $this->env['debug'] ?? null;
        return ($configDebug || $envDebug) ? true : false;
    }

    public function isCli()
    {
        return in_array(php_sapi_name(), ['cli']);
    }


    public function env($key = null, $default = null)
    {
        if (is_null($key)) {
            return $this->env;
        } else {
            $keys = explode('.', strtolower($key), 2);

            if (count($keys) == 1) {
                return $this->env[$keys[0]] ?? $default;
            } else {
                $item = $this->env[$keys[0]] ?? [];
                return $item[$keys[1]] ?? $default;
            }
        }
    }

    public function outputException($msg = '')
    {
        if ($this->config('app.show_error_msg')) {
            $msg = addslashes($msg);
        } else {
            $msg = '';
        }
        $tplContent = file_get_contents($this->getThinkPath() . DS . 'tpl' . DS . 'exception.html');
        $outputContent = str_replace('{$msg}', $msg, $tplContent);
        echo $outputContent;
    }

    public function end(bool $flush = false)
    {
        $flush ? ob_end_flush() : ob_end_clean();
    }


    //------------------------------------------------ core service
    protected function initCoreService()
    {
        $this->make('middleware', [$this->config('middleware.pipe_type')]);
    }

    protected function bootCoreService()
    {
        $appMiddlewareList = require($this->getAppPath() . DS . 'middleware.php');
        $this->middleware->multipleAdd($appMiddlewareList, 'global');
    }


    //------------------------------------------------ get path
    public function getThinkPath()
    {
        return $this->thinkPath;
    }

    public function getRootPath()
    {
        return $this->rootPath;
    }

    public function getAppPath()
    {
        return $this->appPath;
    }

    public function getEntrancePath()
    {
        return $this->entrancePath;
    }

    public function getConfigPath()
    {
        return $this->configPath;
    }

    public function getRuntimePath()
    {
        return $this->runtimePath;
    }

    public function getRoutePath()
    {
        return $this->routePath;
    }

    public function getViewPath()
    {
        return $this->viewPath;
    }

    //------------------------------------------------ controller
    public function invokeControllerAction(string $controller, string $action, array $arguments = [])
    {
        if (strstr($controller, '\\') == false) {
            $controller = $this->resolveController($controller);
        }

        if (class_exists($controller) == false) {
            if ($this->isDebug()) throw new ErrorException('控制器不存在[' . $controller . ']');

            $controller = $this->config('app.safety_net_controller', $this->defaultController);
        }

        $beforeAction = $this->getBeforeAction($controller, $action);
        $afterAction = $this->getAfterAction($controller, $action);

        $controller = $this->make($controller);
        $middleware = $this->pickControllerMiddleware($controller, $action);

        $data = $this->middleware->multipleAdd($middleware, 'controller')
            ->pipeline('controller')
            ->send($this->request)
            ->then(function () use ($controller, $action, $beforeAction, $afterAction, $arguments) {

                try {
                    $beforeAction && $this->invokeMethod([$controller, $beforeAction], $arguments);
                    $data = $this->invokeMethod([$controller, $action], $arguments);
                    $afterAction && $this->invokeMethod([$controller, $afterAction], $arguments);
                } catch (Throwable $e) {
                    throw new RuntimeException(
                        $e->getMessage(),
                        $e->getCode(),
                        $e->getFile(),
                        $e->getLine()
                    );
                }

                $res = ($data instanceof Response) ? $data : Response::create($data, 'html');
                return $res;
            });

        return $data;
    }

    public function invokeClassMethod(string $class, string $method, array $arguments = [])
    {
        return $this->invokeMethod([$class, $method], $arguments);
    }

    public function invokeStaticMethod(string $class, string $method, array $arguments = [])
    {
        return $this->invokeMethod($class . '::' . $method, $arguments);
    }

    public function invokeMethod($callable, array $araguments = [])
    {
        return parent::invokeMethod($callable, $araguments);
    }

    public function invokeFunction($fn, array $arguments = [])
    {
        return parent::invokeFunction($fn, $arguments);
    }

    public function invokeCommand(string $class, string $method, array $params = [])
    {
        $beforeExec = $this->assembleAndCheckMethod($class, 'exec', 'before_');
        $afterExec = $this->assembleAndCheckMethod($class, 'exec', 'after_');

        try {
            $this->invokeMethod([$class, $beforeExec], $params);
            $data = $this->invokeMethod([$class, $method], $params);
            $this->invokeMethod([$class, $afterExec], $params);
        } catch (Throwable $e) {
            throw new RuntimeException(
                $e->getMessage(),
                $e->getCode(),
                $e->getFile(),
                $e->getLine()
            );
        }
        return $data;
    }

    //------------------------------------------------ protected
    protected function prepare()
    {
        define('DS', DIRECTORY_SEPARATOR);
    }

    protected function ditectPaths()
    {
        $this->thinkPath = __DIR__ . DS;
        $this->rootPath = dirname($this->thinkPath, 2) . DS;
        $this->appPath = $this->rootPath . $this->appForder . DS;
        $this->entrancePath = $this->rootPath . 'public' . DS;
        $this->configPath = $this->rootPath . 'config' . DS;
        $this->runtimePath = $this->rootPath . 'runtime' . DS;
        $this->routePath = $this->rootPath . 'route' . DS;
        $this->viewPath = $this->appPath . 'view' . DS;
    }

    protected function registerExceptionHandler()
    {
        $this->make(ExceptionHandler::class)->init($this);
    }

    protected function loadFiles()
    {
        $this->loadEnvFile();
        $this->initAndLoadConfig();

        if (file_exists($provider = $this->appPath . DS . 'provider.php')) {
            $bindings = include($provider);
            is_array($bindings) && $this->bindMultiple($bindings);
        }

        if (file_exists($eventConf = $this->appPath . DS . 'event.php')) {
            $this->event->applyConfig(include($eventConf));
        }
    }

    protected function loadEnvFile()
    {
        $envFile = $this->getRootPath() . '.env';
        if (file_exists($envFile) == false) return;

        $envData = parse_env(file($envFile) ?: []);
        $this->env = $envData;
    }

    protected function initAndLoadConfig()
    {
        $this->make('config', [$this->configPath]);
    }

    protected function config()
    {
        $args = func_get_args();
        return call_user_func_array([$this->config, 'config'], $args);
    }

    protected function resolveController($controllerName)
    {
        $controllerName = trim(trim($controllerName), '.');
        $npList = [$this->appNamespace, 'controller'];
        $npList = array_merge($npList, explode('.', $controllerName));
        return implode('\\', $npList);
    }

    protected function getBeforeAction($controller, $action)
    {
        return $this->assembleAndCheckMethod($controller, $action, 'before_');
    }

    protected function getAfterAction($controller, $action)
    {
        return $this->assembleAndCheckMethod($controller, $action, 'after_');
    }

    protected function pickControllerMiddleware(object $controller, string $action): array
    {
        $allList = $controller->middleware ?? [];
        $pickList = [];
        foreach ($allList as $k => $v) {
            if (is_numeric($k)) {
                $pickList[] = $v;
            } else {
                if (isset($k['only']) && in_array($action, $k['only'])) {
                    $pickList[] = $k;
                }
                if (isset($k['except']) && in_array($action, $k['except']) == false) {
                    $pickList[] = $k;
                }
            }
        }
        return $pickList;
    }

    //------------------------------------------------ private
    private function assembleAndCheckMethod($class, $method, $prefix = '', $suffix = '')
    {
        $fnName = $prefix . $method . $suffix;
        return method_exists($class, $fnName) ? $fnName : null;
    }
}
